学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

Stanford CS193p - [10-12]

Developing Applications for iOS using SwiftUI

Total Lecture: 15

current: [10-12]

Emoji Art

Start a brand-new app called EmojiArt

Positioin Modifier

Position就是你如何在视图空间内定位某物

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private var documentBody: some View {
GeometryReader{ geometry in
ZStack {
...
ForEach(document.emojis) { emoji in
Text(emoji.string)
.position(emoji.position.in(geometry))
}
}
}
}

extetnsion EmojiArt.Emoji.Position {
func `in`(_ geometry: GeometryProxy) -> CGPoint {
let center = geometry.frame(in: .loacl).center //在设备的全局坐标系中获取frame,但我们希望它位于我们视图的局部坐标系中,并且想要那个center
return CGPoint(x: center.x + CGFloat(x), y: center.y - CGFloat(y))
}
}

Drag and Drop

Transferable协议

1
Text(emoji).draggable(emoji) // emojj is a String

dropDestination

1
2
3
4
5
6
7
8
9
10
@State private var highlighted = false

Rectangle()
.stroke(lineWidth: highlighted ? 5 :1)
.dropDestination(for: String.self) { [String], CGPoint in
// deal with those Strings being dropped at CGPoint in the Rectangle
} isTargeted: { isTargeted: Bool in
// highlight something or otherwise indicate a drop is overhead
highlighted = isTargeted
}

Gestures, 2nd MVVM

Making your Views Recognize Gestures

1
myView.gesture(theGesture) //theGesture must implement the Gesture prtocol

Creating a Gesture

1
2
3
var theGesture: some Gesture {
return TapGesture(count: 2)
}

离散手势的识别处理

处理离散手势的方式

1
2
3
4
5
6
7
var theGesture: some Gesture {
return TapGesture(count: 2)
.onEnded { /* do something */}
}

myView.onTapGesture(count: Int){ /* do something */ }
myView.onLongPressGesture(...){ /* do something */ }

Discrete 离散的
TapGesture 是离散手势

非离散手势

1
2
3
4
var theGesture: some Gesture {
DragGesture(...)
.onEnded { value in}
}

非离散手势处理

1
2
3
4
5
6
7
8
9
@GestureState var myGestureState: MyGestureStateType = <staring value>

var theGesture: some Gesture {
DragGesture(...)
.updating($myGestureState) { value,myGestureState,transaction in
myGesture = /*do something related to value */
}
.onEnded { value in /*do something*/ }
}

This var will always return to <starting value> when the gesture ends.

Zoom Gesture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pricate var documentBody: some View {
ZStack{
...
documentContents(in: geometry)
.scaleEffect(zoom * gestureZoom)
.offset(pan)
}
.gesture(zoomGesture)
...
}


@State private var zoom: CGFloat = 1
@State private var pan: CGRect = .zero
@GestureState private var gestureZoom: CGFloat = 1

private var zoomGesture: some Gesture {
MagnificationGesture()
.updating($gestureZoom) { inMotionPrinchScacle, gestureZoom, _ in
gestureZoom = inMotionPrinchScale
}
.onEnded { endingPinchScale in
zoom *= endingPinchScale
}
}

有些手势会消耗手指动作,然后你才能进行其他手势

1
.gesture(panGesture.simultaneously(with: zoomGesture))

告诉系统同时识别这两个手势

1
2
3
4
5
extension CGOffset {
static func +=(lhs: inout CGOffset,rhs: CGOffset) {
lhs = lhs + rhs
}
}

inout 当该函数结束时,参数会被复制回来

Multiple MVVMs

Palette

1
2
3
4
5
6
7
struct Palette: Identifiable {
var name: String
var emojis: String

let id = UUID()

}

UUID是一个生成唯一Id的小结构体,16个字节,随机生成,可哈希

何时使用?

当你拥有的东西数量相当少的时候

environmentObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@main
struct Emoji_ArtApp: App {
@StateObject var paletteStore = PaletteStore(named: "Main")

var body: some Scene {
WindowGroup {
EmojiArtDocumentView(...)
.environmentObject(paletteStore)
}
}
}

struct PaletteChooser: View {
@EnvironmentObject var store: PaletteStore
}

environmentObject 表示这个对象paletteStore被注入到所有的视图中

向上滑动消失动画

1
2
3
4
5
6
7
func view(for palette: Palette) -> some View {
HStack{
Text(palette.name)
ScrollingEmojis(palette.emojis)
}
.transition(.asymmmetric(insertion: .move(edge: .bottom), removal: .move(edge:.top)))
}

为什么转变不起作用?

过渡动画是什么?视图的来来去去,对吗?这个HStack永远不会来来去去,只是这个调色板的名称,标签符号正在发生变化。它不会被删除。

1
2
3
4
5
6
7
8
func view(for palette: Palette) -> some View {
HStack{
Text(palette.name)
ScrollingEmojis(palette.emojis)
}
.id(palette.id)
.transition(.asymmmetric(insertion: .move(edge: .bottom), removal: .move(edge:.top)))
}

这样一来HStack身份就成为了其本身的一部分,这个调色板出现并且它是一个具有不同ID的新调色板时,整个视图都必须被替换,因为这是一种不通的视图

.clipped

如何避免动画超出视图绘制?

1
2
3
4
5
var body: some View{
chooser
...
}
.clipped()

@escaping

1
2
3
4
5
struct AnimatedActionButton: View {
init(_ title: String? = nil, systemImage: String? = nil, role: ButtonRole? = nil, action: @escaping() -> Void) {
...
}
}

@escaping意味着这个函数init,将会保留这个闭包并在稍后调用它,所以它从这个init中逃脱,
为什么Swift想知道这一点?
因为它要内联无法逃脱的闭包,在这种情况下,它必须将闭包转换成引用类型,将它们放在堆中,并为它们提供一个指针,这样它们就可以被保存。

Persistence, Property Wrappers

FileSystem

sandbox

Application directory – Your executable,.jpgs,ect.; not writaable

Documents directory – Permanenet storage created by and always visiable to the user
Application Support directory - Permanent storage not seen directly by the user
Caches directory - Store temprary files here (this is not backed up)

Accessing the file system

1
2
3
URL.documentDirectory
URL.applicationSupportDirectory
URL.cachesDirectory
1
2
3
4
func appendingPathComponent(String) -> URL
func appendingPathExtension(String) -> URL // e.g. "jpg"

let path = URL.documentsDirectory.appendingPathComponent("fulename.doc")
1
2
var ifFileURL: Bool // is this a file URL (whether file exists or not) or something else?
func resourceValues(for keys: [URLResourceKey]) throws -> [URLResourceKey: Any]?

example keys: .creationDateKey,.isDirectoryKey,.fileSizeKey

Data

Reading

1
2
3
init(contentsOf: URL, options: Data.ReadingOptions) throws

let data = try Data(contentOf: url)

Writing

1
func write(to url: URL, options: Data.WritingOptions) thros -> Bool

FileManager

用于管理文件系统本身,复制文件,创建目录,列出目录

1
fileExists(atPath: String) -> Bool

Codable Mechanism

如何创建想要放入文件系统的数据呢?

Codable本质上是一种收集结构体所有变量并将其分解成数据的方法(like a JSON Data)

1
2
let object: MyType = ... //MyType must conrom to Codable
let jsonData: Data = try JSONEncoder().encode(object)
1
2
3
4
5
6
7
let jsonString = String(data: jsonData, encoding: .utf8) // JSON is always utf8

try jsonData.write(to: url)

if let myObject: MyType = try? JSONDecoder().decode(MyType.self, from: jsonData){
...
}

UserDefaults

dictionary-like thing

1
2
3
4
let defaults = UserDefaults.standard

defaults.set(object, forKey: "SomeKey") // object must be a Property List

Property List不是某种协议,或某种具体类型

UserDefaults不会在你每次存储时都写入磁盘,而是缓冲更改并在它认为你可能需要时将其写入。
切换App时会写入

Error Handing

为什么需要throw这些错误?
如果没有throw错误,那么函数就必须返回一个错误代码,现在你要返回什么?
要么是一个有成功和失败的枚举,要么像一个有正确答案然后有错误的元组,这会变得混乱。

1
2
let imageData = try? Data(contentsOf: url) //imageData = nil if error thrown
try? write(to: url) // ignores any error thrown
1
2
3
func foo() throws {
try somethingThatThrows()
}

try foo()

handing a thrown error

1
2
3
4
5
6
do {
try functionThatThrows()
} catch let error {
// handle the thrown error here
// error can be anything that implements the Error protocol(localizedDescription)
}

值类型可以用EmojiArt.self替换它们的整个自我

@Published

1
2
3
4
5
6
7
8
9
10
11
12
class PaletteStore: ObervableObject {
let name: String

@Published var palettes: [Palette] {
get {
UserDefaults.standrad.palettes(forKey: name)
}
set {
UserDefaults.standrad.set(newValue, forKey: name)
}
}
}

属性包装器不能用于计算属性

如果你有一个类并且实现了ObservableObject,那么你就会得到一个免费的变量var objectWillChange: ObservableObjectPublisher,它用来告诉视图某些东西将会改变,你可能需要更新,这就是@Published所做的。

@Published每当你更改该变量时,它都会调用该变量的发送函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
class PaletteStore: ObervableObject {
let name: String
var objectWillChange: ObservableObjectPublisher
var palettes: [Palette] {
get {
UserDefaults.standrad.palettes(forKey: name)
}
set {
UserDefaults.standrad.set(newValue, forKey: name)
objectWillChange.send()
}
}
}

Property Wrappers

all those @ things

属性包装器实际创建的是一个结构体,这些结构体封装了你始终希望应用于某个变量的某种行为。

@ObservedObject当它看到objectWillChange,send()时,会导致视图重新绘制

属性包装器时Swift语言中的一个特性,它允许你通过语法糖的方式创建和使用这些结构体

属性包装器语法糖

1
2
3
4
5
6
@Published var emojiArt: EmojiArt = EmojiArt()

struct Published {
var wrappedValue: EmojiArt
var projectedValue: Publisher<EmojiArt, Never>
}
1
2
3
4
5
var _emojiArt: Published = Published(wrappedValue: EmojiArt())
var emojiArt: EmojiArt {
get { _emojiArt.wrappedValue }
set { _emojiArt.wrappedValue = newValue }
}

可以通过$emojiArt的方式获取projectedValue ,它可以是结构体想要的任何东西,不同的属性包装器有不同的projectValue

publisher本质上是Swift里知道如何在流上发布事物的东西,它发布的是信息流

Why?

the Wrapper struct does something on set/get of the wrappedValue

@Published

当它的wrappedValue被设置时( is set),它会通过projectedValue($smojiArt),那个发布者(Publisher),发布变化,

它也会在其封闭的ObservableObject中调用objectWillChange,send()

@State

The wrappedValue is : any type

它会导致wrappedValue被移动到堆中,并在视图不断变化时稳定地保存在那里,该值发生变化时,它就会使视图无效,进行重绘。

Projected value: a Binding to the value in the heap

1
2
3
4
@State private var foo: Int
init() {
_foo = .init(initialValue: 5)
}

@StateObject and @ObservedObject

The wrapedValue必须是一个class,基本上就是你的ViewModel,anything that implements the ObservableObject protocol

wrappedValue执行objectWillChange.send()时,它们会使View无效

可以使用Binding来访问Viewmodel所有变量

1
$myViewModel.someVar.someArray[3]

@Binding

一个绑定到其他东西的值,当你获取并设置其wrappedValue时,它会获取并设置它所绑定到的事物的值,它是一个引用,当绑定的值改变时,它会使视图无效。

Projected value: a binding to the Binding itself

1
2
3
4
5
6
7
8
9
10
11
12
13
struct MyView: View {
@State var myString = "Hello"
var body: View {
OtherView(sharedText: $myString)
}
}

struct OtherView: View {
@Binding var sharedText: String
var body: View {
TextField("Shared: ", text: sharedText)
}
}

Binding.constant(value)

Binding(get:,set:)

@EnvironmentObject

它的wrappedValue是通过.environmentObject()注入到ViewObservableObject,当wrappedValue发生改变时,它可以使视图无效

@Environment

1
@Environment(\.colorSheme) var colorScheme

将创建一个名为colorScheme的变量,代表当前正在运行的进程的配色方案(暗模式或亮模式)

\.colorSheme is a key path

你可以在EnvironmentValues结构体中找到所有的可用的key

the wrappedValue is: the value of some var in EnvironmentValues

What it does: gets/sets a value of some var in EnvironmentValues

Projected value: none,所以无法使用binding